package vue;

import io.ClientServer;
import io.AsyncListener;
import io.ListenerManager;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;

import controller.ControllerCle;
import metier.Cle;

/**
 * Fenêtre principale.
 * Permet de lancer un serveur ou 
 * un client en mode graphique.
 */
@SuppressWarnings("serial")
public class MainWindow extends JFrame
{
	// Expression régulière pour parser un entier.
	// Les espaces au début/à la fin sont autorisés.
	private final static Pattern PATTERN_INTEGER = Pattern.compile("^\\s*([0-9]+)\\s*$");
	private final static int PORT_MIN = 0;	// Plus petit numéro de port valide
	private final static int PORT_MAX = 65535;	// Plus grand numéro de port valide
	private final static String[] TYPES = { "Serveur", "Client" };	// Types de gestionnaires disponibles
	private final static String TITLE_PREFIX = "RSA ";	// Préfixe utilisé dans le titre de la fenêtre
	
	private ClientServer _clientServer;		// Gestionnaire des opérations réseau (socket)
	
	private JLabel _typePreLabel;	// Libellé préfixant la liste déroulante du type de gestionnaire réseau
	private JLabel _typeLastLabel;	// Libellé suivant la liste déroulante du type de gestionnaire réseau
	private JTextField _portField;	// Champs de texte réservé au numéro de port
	private JTextArea _inputArea;	// Zone de texte contenant un message à envoyer
	private JTextArea _outputArea;	// Zone de texte contenant les messages reçus
	private JTextArea _consoleArea;	// Zone de texte détaillant les opérations de chiffrement/déchiffrement
	
	private JComboBox _typeComboBox;	// Liste déroulante permettant de choisir le type du gestionnaire réseau (client ou serveur)
	private JButton _startButton;	// Bouton de lancement du gestionnaire réseau
	private JButton _sendButton;	// Bouton d'envoi d'un message
	
	private int _port;	// Numéro de port actuel
	private ControllerCle _ctrlCle ;
	private Cle _publicReceivedKey = null ;
	private boolean keyShared ; 
	
	/**
	 * Constructeur
	 */
	public MainWindow()
	{
		_ctrlCle = new ControllerCle();
		_ctrlCle.generateKey();
		
		keyShared = false ;
		
		// Application du look and feel systeme.
		try {
			UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
		}
		catch (Exception e) { e.printStackTrace(); }

		// Paramétrage de la fenêtre
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setSize(new Dimension(450, 300));
				
		// Création de l'interface graphique
		initializeComponents();
		
		// Initialisation du numéro de port
		int initialPort = ClientServer.DEFAULT_PORT;
		String initialPortStr = String.valueOf(initialPort);
		_portField.setText(initialPortStr);
		_port = initialPort;
		
		// Initialisation du type sélectionné
		int selectedType = 0;
		_typeComboBox.setSelectedIndex(selectedType);
		setTitleFromType(selectedType);
		
		// Désactivation des composants interdits
		setAreasEnabled(false);
	}
	
	/**
	 * Initialise, arrange et prépare les gestionnaires 
	 * d'évènements pour l'ensemble des composants graphiques.
	 */
	private void initializeComponents()
	{
		Dimension cmbBoxSize = new Dimension(100, 30);
		
		_typeComboBox = new JComboBox(TYPES);
		_typeComboBox.setPreferredSize(cmbBoxSize);
		_typeComboBox.setMinimumSize(cmbBoxSize);
		_typeComboBox.setSize(cmbBoxSize);
		_typeComboBox.setMaximumSize(cmbBoxSize);
		_typeComboBox.addItemListener(new ItemListener()
		{
			@Override
			public void itemStateChanged(ItemEvent e)
			{
				typeComboBoxItemChanged(_typeComboBox.getSelectedIndex());
			}
		});
		
		Dimension portSize = new Dimension(53, cmbBoxSize.height);
		
		_portField = new JTextField();
		_portField.setPreferredSize(portSize);
		_portField.setMinimumSize(portSize);
		_portField.setSize(portSize);
		_portField.setMaximumSize(portSize);
		_portField.getDocument().addDocumentListener(new DocumentListener()
		{
			private void notifyTextChanged()
			{ 
				portFieldTextChanged(_portField.getText());
			}
			@Override
			public void removeUpdate(DocumentEvent e) { notifyTextChanged(); }
			@Override
			public void insertUpdate(DocumentEvent e) { notifyTextChanged(); }
			@Override
			public void changedUpdate(DocumentEvent e) { notifyTextChanged(); }
		});
		
		Dimension buttonSize = new Dimension(75, cmbBoxSize.height);
		
		_startButton = new JButton("Lancer");
		_startButton.setPreferredSize(buttonSize);
		_startButton.setMinimumSize(buttonSize);
		_startButton.setSize(buttonSize);
		_startButton.setMaximumSize(buttonSize);
		_startButton.addActionListener(new ActionListener() 
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				startButtonClicked();
			}
		});
		
		_typePreLabel = new JLabel("Exécuter un");
		_typeLastLabel = new JLabel("sur le port");
		
		Box hB_type = Box.createHorizontalBox();
		hB_type.add(Box.createHorizontalStrut(10));
		hB_type.add(_typePreLabel);
		hB_type.add(Box.createHorizontalStrut(7));
		hB_type.add(_typeComboBox);
		hB_type.add(Box.createHorizontalStrut(7));
		hB_type.add(_typeLastLabel);
		hB_type.add(Box.createHorizontalStrut(7));
		hB_type.add(_portField);
		hB_type.add(Box.createHorizontalStrut(21));
		hB_type.add(_startButton);
		hB_type.add(Box.createHorizontalGlue());
		
		_inputArea = new JTextArea();
		_inputArea.setWrapStyleWord(true);
		_inputArea.setLineWrap(true); 
		JScrollPane inputPane = new JScrollPane(_inputArea);
		
		Box hB_inputLabel = Box.createHorizontalBox();
		hB_inputLabel.add(new JLabel("Message :"));
		hB_inputLabel.add(Box.createHorizontalGlue());
		
		_sendButton = new JButton("↪");
		_sendButton.addActionListener(new ActionListener() 
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				sendButtonClicked();
			}
		});
		
		Box hB_inputArea = Box.createHorizontalBox();
		hB_inputArea.add(inputPane);
		hB_inputArea.add(_sendButton);
		
		Box vB_input = Box.createVerticalBox();
		vB_input.add(hB_inputLabel);
		vB_input.add(hB_inputArea);
		
		_outputArea = new JTextArea();
		_outputArea.setEditable(false);
		_outputArea.setBackground(new Color(245, 245, 245));
		_outputArea.setWrapStyleWord(true);
		_outputArea.setLineWrap(true); 
		JScrollPane outputPane = new JScrollPane(_outputArea);
		
		Box hB_outputLabel = Box.createHorizontalBox();
		hB_outputLabel.add(new JLabel("Conversation :"));
		hB_outputLabel.add(Box.createHorizontalGlue());
		
		Box hB_outputArea = Box.createHorizontalBox();
		hB_outputArea.add(outputPane);
		
		Box vB_output = Box.createVerticalBox();
		vB_output.add(hB_outputLabel);
		vB_output.add(hB_outputArea);
		
		_consoleArea = new JTextArea();
		_consoleArea.setEditable(false);
		_consoleArea.setForeground(Color.WHITE);
		_consoleArea.setBackground(Color.BLACK);
		_consoleArea.setWrapStyleWord(true);
		_consoleArea.setLineWrap(true); 
		JScrollPane consolePane = new JScrollPane(_consoleArea);

		Box hB_consoleLabel = Box.createHorizontalBox();
		hB_consoleLabel.add(new JLabel("Console :"));
		hB_consoleLabel.add(Box.createHorizontalGlue());
		
		Box hB_consoleArea = Box.createHorizontalBox();
		hB_consoleArea.add(consolePane);
		
		Box vB_console = Box.createVerticalBox();
		vB_console.add(hB_consoleLabel);
		vB_console.add(hB_consoleArea);
		
		JSplitPane inOutSplit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, vB_input, vB_output);

		JSplitPane consoleInOutSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT, inOutSplit, vB_console);
		
		Box hB_areas = Box.createHorizontalBox();
		hB_areas.add(Box.createHorizontalStrut(10));
		hB_areas.add(consoleInOutSplit);
		hB_areas.add(Box.createHorizontalStrut(10));
		
		Box vB_main = Box.createVerticalBox();
		vB_main.add(Box.createVerticalStrut(10));
		vB_main.add(hB_type);
		vB_main.add(Box.createVerticalStrut(10));
		vB_main.add(hB_areas);
		vB_main.add(Box.createVerticalStrut(10));
				
		add(vB_main);
		
		inOutSplit.setResizeWeight(0.525);
		inOutSplit.setDividerLocation(0.525);
		
		consoleInOutSplit.setResizeWeight(0.3);
		consoleInOutSplit.setDividerLocation(0.3);
	}
	
	/**
	 * Active/désactive les zones de texte accessibles par l'utilisateur.
	 * @param enabled
	 * True pour activer, false pour désactiver.
	 */
	private void setAreasEnabled(boolean enabled)
	{
		_inputArea.setEnabled(enabled);
		_outputArea.setEnabled(enabled);
		_sendButton.setEnabled(enabled);
		_consoleArea.setEnabled(enabled);
	}
	
	/**
	 * Définit le titre complet de la fenêtre en accord avec le port choisi.
	 * @param typeIndex
	 * Indice de base 0 du type de gestionnaire réseau (serveur ou client).
	 */
	private void setTitleFromPort(int port)
	{
		String title = TITLE_PREFIX + getTypeTitle(_typeComboBox.getSelectedIndex()) + ' ' + getPortTitle(port);
		setTitle(title);
	}
	
	/**
	 * Définit le titre complet de la fenêtre en accord avec le type choisi.
	 * @param typeIndex
	 * Indice de base 0 du type de gestionnaire réseau (serveur ou client).
	 */
	private void setTitleFromType(int typeIndex)
	{
		String title = TITLE_PREFIX + getTypeTitle(typeIndex) + ' ' + getPortTitle(_port);
		setTitle(title);
	}
	
	/**
	 * Obtient la portion de titre pour la fenêtre en accord avec le type choisi.
	 * @param typeIndex
	 * Indice de base 0 du type de gestionnaire réseau (serveur ou client).
	 * @return
	 * Portion de titre pour la fenêtre en accord avec le type choisi.
	 */
	private String getTypeTitle(int typeIndex)
	{
		String title = "";
		if (TYPES != null && 0 <= typeIndex && typeIndex < TYPES.length)
		{
			title = TYPES[typeIndex];
		}
		return title;
	}
	
	/**
	 * Obtient la portion de titre de la fenêtre en accord avec le port choisi.
	 * @param port
	 * Numéro de port compris entre PORT_MIN et PORT_MAX.
	 * @return
	 * Portion de titre pour la fenêtre en accord avec le port choisi.
	 */
	private String getPortTitle(int port)
	{
		String title = "";
		if (_port > 0)
		{
			title += "[" + String.valueOf(_port) + "]";
		}
		return title;
	}
	
	/**
	 * Appelé lorsque l'index de l'item sélectionné dans la liste déroulante 'type' change.
	 * @param selectedIndex
	 * Indice de l'item sélectionné, ou -1 si aucun item n'est sélectionné.
	 */
	private void typeComboBoxItemChanged(int selectedIndex)
	{
		// Un item est sélectionné
		if (selectedIndex > -1)
		{
			setTitleFromType(selectedIndex);
		}
	}
	
	/**
	 * Appelé lors d'un clic sur le bouton 'Lancer'.
	 */
	private void startButtonClicked()
	{
		String typePreLabelTxt = 
				getTypeTitle(_typeComboBox.getSelectedIndex()) +
				" lancé " +
				_typeLastLabel.getText() + ' ' +
				String.valueOf(_port);
				
		_typePreLabel.setText(typePreLabelTxt);
		_typeComboBox.setVisible(false);
		_typeLastLabel.setVisible(false);
		_portField.setVisible(false);
		_startButton.setEnabled(false);
		
		AsyncListener initListener = new AsyncListener()
		{
			// Appelé une seule fois à l'initialisation de ce client/serveur
			@Override
			protected void onEvent(Object received)
			{
				Cle pubKey = _ctrlCle.getCle_pub();
				
				// Envoi de la clé publique
				_clientServer.Send(pubKey);
				
				// Notification dans la console
				_consoleArea.setText(_consoleArea.getText() + 
						"[ENVOIE]\nclé publique:" + 
						"\n\t u = " + pubKey.getM_u().toString() +
						"\n\t e = " + pubKey.getM_e().toString() +
						"\n\t n = " + pubKey.getM_n().toString() + "\n");
			}
		};
		
		AsyncListener receiveListener = new AsyncListener()
		{
			// Appelé chaque fois qu'un message est reçu par ce client/serveur
			@Override
			protected void onEvent(Object received)
			{
				if (received != null)
				{
					// Les clés publiques n'ont pas encore été échangées
					if(!keyShared)
					{
						// L'objet reçu message est la clé publique
						_publicReceivedKey = (Cle) received ;
						
						// Notification dans la console
						_consoleArea.setText(_consoleArea.getText() + 
								"[RECOIT]\nclé publique:" + 
								"\n\t u = " + _publicReceivedKey.getM_u().toString() +
								"\n\t e = " + _publicReceivedKey.getM_e().toString() +
								"\n\t n = " + _publicReceivedKey.getM_n().toString() + "\n");
						
						keyShared = true;
					}
					// Les clés publiques ont déjà été échangées
					else
					{
						// L'objet reçu est un message crypté
						String[] tabChiffre = (String[]) received;
						
						// Le message est valide
						if (tabChiffre != null && tabChiffre.length > 0)
						{							
							// Décryptage du message reçu à l'aide de la clé privée
							String message = _ctrlCle.dechiffrement(tabChiffre);
										
							// Création de la chaine de notification
							String display = _consoleArea.getText();
							display += "[RECOIT]\n";
							for (int index = 0; index < tabChiffre.length; index++)
							{
								display += tabChiffre[index] + "  ";
							}
							display += "\n\"" + message + "\"\n";
							
							// Notification dans la console
							_consoleArea.setText(display);
							
							// Affichage du message décrypté dans les messages reçus
							_outputArea.setText(_outputArea.getText() + "L'autre: " + message + "\n");
						}
						// Le message est invalide
						else
						{
							// Notification dans la console
							_consoleArea.setText(_consoleArea.getText() + 
									"[RECOIT]\n[ERREUR] message invalide.\n");
						}
					}
				}
			}
		};
		
		AsyncListener disposeListener = new AsyncListener()
		{
			@Override
			protected void onEvent(Object param)
			{
				JOptionPane.showMessageDialog(null, "L'utilisateur distant s'est déconnecté.");
			}
		};
		
		ListenerManager manager = new ListenerManager(initListener, receiveListener, disposeListener);
		_clientServer = new ClientServer(_port, manager);
		
		// Server
		if (_typeComboBox.getSelectedIndex() == 0)
		{
			_clientServer.launchAsServer();
		}
		// Client
		else if (_typeComboBox.getSelectedIndex() == 1)
		{
			_clientServer.launchAsClient();
		}
		
		setAreasEnabled(true);
		
	}
	
	/**
	 * Appelé lors d'un clic sur le bouton '↪'.
	 */
	private void sendButtonClicked()
	{		
		// Récupération du texte saisi par l'utilisateur
		String msg = _inputArea.getText();
		
		// La clé publique a été obtenue
		if(_publicReceivedKey != null && msg != null && msg != "")
		{
			// Chiffrement du texte avec la clé publique
			String[] tabChiffre = _ctrlCle.chiffrement(msg, _publicReceivedKey);
			if (tabChiffre != null && tabChiffre.length > 0)
			{
				// Envoi du texte chiffré
				_clientServer.Send(tabChiffre);
				
				// Création de la chaine de notification
				String display = _consoleArea.getText();
				display += "[ENVOIE] \n\"" + msg + "\"\n";
				for (int index = 0; index < tabChiffre.length; index++)
				{
					display += tabChiffre[index] + "  ";
				}
				display += "\n";
				
				// Notification dans la console
				_consoleArea.setText(display);
			}
		}
		_outputArea.setText(_outputArea.getText() + "Moi: " + msg + "\n");
		_inputArea.setText("");
	}
	
	/**
	 * Appelé lors d'une modification du texte du champs 'numéro de port'.
	 * @param text
	 * Texte contenu dans le champs du numéro de port.
	 */
	private void portFieldTextChanged(String text)
	{
		// Le champs n'est pas vide
		if (text != null)
		{
			Matcher match = PATTERN_INTEGER.matcher(text);
			
			// Le champs contient un nombre
			if (match.matches())
			{
				// Récupération du nombre
				String nbrStr = match.group(1);
				int nbr = Integer.parseInt(nbrStr);
				
				// Le nombre est dans la plage des numéros de ports
				if (PORT_MIN <= nbr && nbr <= PORT_MAX)
				{
					_port = nbr;
					_startButton.setEnabled(true);
					setTitleFromPort(_port); // Mise à jour du titre
					return;
				}
			}
		}
		_port = -1;
		_startButton.setEnabled(false);
		setTitleFromPort(_port); // Mise à jour du titre
	}
	
}
